﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Web.Services;
using System.Globalization;

using BizElements.BusinessLayer;
using BizElements.Web;

namespace BizElements.Web.UI
{
    #region Documentation.
    /// <summary>
    /// Uses <see cref="SearchProvider"/> objects registered in <see cref="SearchProviderRegistry"/> to return 
    /// completion lists and key values for <see cref="BoundAutoComplete"/> and <b>AutoCompleteExtender</b> controls.
    /// </summary>
    /// <remarks>
    /// <para>Web service uses <see cref="SearchProvider"/> instances registered in <see cref="SearchProviderRegistry"/> to perform a search.
    /// A search provider is uniquely identifed by a key which is provided to Web services methods through <b>contextKey</b> parameter.</para>
    /// <para>The service will use the first writable <see cref="ISearchEngineField"/> it finds in the <see cref="ISearchEngine"/> provided
    /// by selected <see cref="SearchProvider"/> instance. Only one search field may be used. If you want to search through multiple fields then 
    /// you will have to concatenate values of multiple fields in the data source (eg. create a view) and use the <see cref="TextSearchMode.AnywhereInText"/> 
    /// search mode or develope a specialized search engine. Note that, this may cause performance issues with large quantity of data.</para>
    /// <para>User data, i.e. actor and globalization data, is provided by <b>UserSession</b> static class. This requires that the Web service is hosted
    /// in the same application domain as the Web application (UI).</para>
    /// <para>The behavior of the service may be configured through search provider's extended properties (see <see cref="SearchProvider.ExtendedProperties"/>).
    /// The <see cref="SearchWsConfiguration"/> encapsulates all supported properties and makes it easier to configure the service.</para>
    /// </remarks>
    #endregion
    [WebService(Namespace = "http://bizblocks.com/BizElements.Web.UI/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [System.ComponentModel.ToolboxItem(false)]
    [System.Web.Script.Services.ScriptService]
    /// <summary>Web service which returns the word completions and key values.</summary>
    public class SearchWs : System.Web.Services.WebService
    {
        private static IBindableField GetFirstDisplayField(ISearchEngine engine)
        {            
            return ArrayUtil.Find<IBindableField>(engine.DisplayFields, (f) => IsVisible(f));
        }

        #region GetCompletionList.

        /// <summary>
        /// Gets completion list using registered search providers.
        /// </summary>
        /// <param name="prefixText">Value to search for in the data source. It is passed to <see cref="ISearchEngineField.BeginValue"/> property of the first visible and editable filter field.</param>
        /// <param name="count">Maximum numbers of records to retrieve from the data-source.</param>
        /// <param name="contextKey">Search provider key.</param>
        /// <returns>Data that matches the user defined criteria.</returns>
        [WebMethod(/*enableSession*/ true)]
        public string[] GetCompletionList(string prefixText, int count, string contextKey)
        {
            var engineCfg = CreateEngineAndConfig(contextKey);
            ISearchEngine engine = engineCfg.First;
            var cfg = engineCfg.Second;
            // SearchFieldParser is not used when getting completion list because it would limit it.
            DataView data = Search(prefixText, count, engine, cfg.SearchFields, null);

            var firstDisplayField = GetFirstDisplayField(engine);
            IMessageFormatter formatter = GetFormatter();
            string[] completionList = ArrayUtil.ToArray<string, DataRowView>(data, (row) => formatter.Format(row[firstDisplayField.BindingName]));
            return completionList;
        }

        #endregion        

        #region GetKeyValue.

        /// <summary>
        /// Gets first key value which matches provided criteria. Uses registered search providers.
        /// </summary>
        /// <param name="prefixText">Value to search for in the data source. It is passed to <see cref="ISearchEngineField.BeginValue"/> property of the first visible and editable filter field.</param>
        /// <param name="contextKey">Search provider key.</param>
        /// <returns>Key value of the first record that matches the provided criteria. <b>null</b> if no matches are found.</returns>
        [WebMethod(/*enableSession*/ true)]
        public string GetKeyValue(string prefixText, string contextKey)
        {
            var engineCfg = CreateEngineAndConfig(contextKey);
            ISearchEngine engine = engineCfg.First;
            var cfg = engineCfg.Second;
            DataView data = Search(prefixText, 1, engine, cfg.SearchFields, cfg.SearchFieldParser);
            string keyField = engine.PrimaryKeyFields[0].BindingName;
            object keyValue = (data.Count > 0) ? data[0][keyField] : null;
            return Convert.ToString(keyValue, CultureInfo.InvariantCulture);
        }

        #endregion

        #region GetKeyAndDisplayValue.

        /// <summary>
        /// Gets first key and display values which match provided criteria. Uses registered search providers.
        /// </summary>
        /// <param name="prefixText">Value to search for in the data source. It is passed to <see cref="ISearchEngineField.BeginValue"/> property of the first visible and editable filter field.</param>
        /// <param name="contextKey">Search provider key.</param>
        /// <returns>Array of strings which contains two items: key and display value of the first record that matches the provided criteria.
        /// Array contains two <b>null</b> references if no matches are found.</returns>
        [WebMethod(/*enableSession*/ true)]
        public string[] GetKeyAndDisplayValue(string prefixText, string contextKey)
        {
            var engineCfg = CreateEngineAndConfig(contextKey);
            ISearchEngine engine = engineCfg.First;
            var cfg = engineCfg.Second;
            DataView data = Search(prefixText, 1, engine, cfg.SearchFields, cfg.SearchFieldParser);
            
            object keyValue = null;
            object displayValue = null;
            if (data.Count > 0)
            {
                string keyField = engine.PrimaryKeyFields[0].BindingName;
                keyValue = data[0][keyField];
                var firstDisplayField = GetFirstDisplayField(engine);
                displayValue = data[0][firstDisplayField.BindingName];
            }

            var formatter = GetFormatter();
            string[] keyValuePair = new string[]
            {
                Convert.ToString(keyValue, CultureInfo.InvariantCulture),
                formatter.Format(displayValue)
            };
            
            return keyValuePair;
        }

        #endregion

        #region CreateEngineAndConfig.

        private static Pair<ISearchEngine, SearchWsConfiguration> CreateEngineAndConfig(string contextKey)
        {            
            SearchProvider provider = SearchProviderRegistry.GetInstance().GetProvider(contextKey);
            if (provider == null)
                throw new InvalidOperationException("Cannot retrieve provider with " + contextKey + " key from the registry.");

            var engineBuilder = provider.CreateEngineBuilderInstance();
            if (engineBuilder == null)
                throw new Exception("Search provider/component does not contain a valid search engine builder.");

            ISearchEngine engine = engineBuilder.CreateSearchEngine(GetCurrentActor());
            SearchWsConfiguration cfg = new SearchWsConfiguration();
            cfg.ReadFromDictionary(provider.ExtendedProperties);

            return new Pair<ISearchEngine, SearchWsConfiguration>(engine, cfg);
        }

        #endregion

        #region Search and set filter.

        private static DataView Search(string prefixTxt, int count, ISearchEngine engine, string[] searchFields, Delegates.Function<IDictionary<string, string>, string> searchFieldParser)
        {
            if (!ArrayUtil.IsNullOrEmpty(searchFields))
            {
                bool canParsePrefix = false;
                if (searchFieldParser != null)
                {
                    var parsedValues = searchFieldParser(prefixTxt);
                    if (!ArrayUtil.IsNullOrEmpty(parsedValues))
                        canParsePrefix = true;
                }

                if (canParsePrefix)
                    SetSelectedParsedFieldsWithOr(prefixTxt, engine, searchFields, searchFieldParser);                    
                else
                    SetSelectedFieldsWithOr(prefixTxt, engine, searchFields);                    
            }
            else
            {
                SetFirstField(prefixTxt, engine);
            }

            engine.MaxNumberOfRecordsReturned = count;
            DataView data = engine.Search();
            return data;
        }

        private static void SetAllFieldsWithOr(string prefixTxt, ISearchEngine engine)
        {
            foreach (ISearchEngineField field in engine.SearchFields)
            {
                if (IsWriteable(field))
                {
                    field.BeginValue = prefixTxt;
                    field.AddWithOr = true;
                }
            }
        }

        private static void SetAllParsedFieldsWithOr(string prefixText, ISearchEngine engine, Delegates.Function<IDictionary<string, string>, string> searchFieldParser)
        {
            var allWriteable = ArrayUtil.FindAll<List<ISearchEngineField>, ISearchEngineField>(engine.SearchFields, (f) => IsWriteable(f));
            var fieldValues = searchFieldParser(prefixText);
            foreach (ISearchEngineField field in allWriteable)
            {
                bool isParsed = fieldValues.Keys.Contains(field.BindingName);
                if (isParsed)
                {
                    field.BeginValue = fieldValues[field.BindingName];
                    field.AddWithOr = true;
                }
            }
        }

        private static void SetSelectedFieldsWithOr(string prefixTxt, ISearchEngine engine, string[] allowedFields)
        {
            foreach (ISearchEngineField field in engine.SearchFields)
            {
                bool isSelectedField = ArrayUtil.Contains<string>(allowedFields, field.BindingName);
                if (isSelectedField && IsWriteable(field))
                {
                    field.BeginValue = prefixTxt;
                    field.AddWithOr = true;
                }
            }
        }

        private static void SetSelectedParsedFieldsWithOr(string prefixText, ISearchEngine engine, string[] allowedFields, Delegates.Function<IDictionary<string, string>, string> searchFieldParser)
        {
            var allWriteable = ArrayUtil.FindAll<List<ISearchEngineField>, ISearchEngineField>(engine.SearchFields, (f) => IsWriteable(f));
            var selectedWriteable = ArrayUtil.FindAll<List<ISearchEngineField>, ISearchEngineField>(allWriteable, (f) => ArrayUtil.Contains<string>(allowedFields, f.BindingName));
            var fieldValues = searchFieldParser(prefixText);
            foreach (ISearchEngineField field in selectedWriteable)
            {
                bool isParsed = fieldValues.Keys.Contains(field.BindingName);
                if (isParsed)
                {
                    field.BeginValue = fieldValues[field.BindingName];
                    field.AddWithOr = true;
                }
            }
        }

        private static void SetFirstField(string prefixTxt, ISearchEngine engine)
        {
            var firstSearchField = ArrayUtil.Find<ISearchEngineField>(engine.SearchFields, (f) => IsWriteable(f));
            engine.SearchFields[firstSearchField.BindingName].BeginValue = prefixTxt;
        }

        #endregion

        #region Util.

        private static bool IsWriteable(ISearchEngineField searchField)
        {
            return !searchField.IsAutoPopulated && !searchField.IsHidden && !searchField.IsReadOnly;
        }

        private static bool IsVisible(IBindableField displayField)
        {
            return !displayField.IsHidden;
        }

        private static IActor GetCurrentActor()
        {
            return UserSession.Actor;
        }

        private static IMessageFormatter GetFormatter()
        {
            return UserSession.Formatter;
        }

        #endregion
    }

    /// <summary>
    /// Configures the behavior of <see cref="SearchWs"/> Web service.
    /// </summary>
    public class SearchWsConfiguration
    {
        #region Constants.

        /// <summary>
        /// Enumerates keys, ie. string constants, of the extended properties (see <see cref="SearchProvider.ExtendedProperties"/>) 
        /// which are used to configure <see cref="SearchWs"/> behavior.
        /// </summary>
        private static class Keys
        {
            /// <summary>Name of the optional extended property (see <see cref="SearchProvider.ExtendedProperties"/>) that 
            /// contains a list of search field names (semicolon delimited) the user entry is passed to.</summary>
            /// <remarks>By default, only the first writeable field in the <see cref="ISearchEngine.SearchFields"/> is used. If the
            /// <b>SearchFieldList</b> property is set then the values is passed to all writeable fields which are parsed from the list.
            /// The <see cref="ISearchEngineField.AddWithOr"/> flag/mode is used.</remarks>
            public const string SearchFieldList = "SearchFieldList";

            /// <summary>Assembly that contains the static method which parses search field values from user input.</summary>
            public const string SearchFieldParserAssembly = "SearchFieldParserAssembly";
            /// <summary>Fully qualified name of the class that contains the static method which parses search field values from user input.</summary>
            public const string SearchFieldParserClass = "SearchFieldParserClass";
            /// <summary>Name of the static method which parses search field values from user input.</summary>
            public const string SearchFieldParserMethod = "SearchFieldParserMethod";
        }

        #endregion

        #region Properties.

        /// <summary>Gets or sets the list of search field names (semicolon delimited) the user entry is passed to.</summary>
        /// <value>Array of string containing</value>
        /// <remarks>By default, only the first writeable field in the <see cref="ISearchEngine.SearchFields"/> is used. If the
        /// <b>SearchFieldList</b> property is set then the values is passed to all writeable fields which are parsed from the list.
        /// The <see cref="ISearchEngineField.AddWithOr"/> flag/mode is used.</remarks>
        public string[] SearchFields { get; set; }

        Delegates.Function<IDictionary<string, string>, string> searchFieldParser;

        /// <summary>Gets or sets the static method which parses search field values from user input.</summary>
        /// <value>A delegate to static method which accepts a single <b>string</b> argument and returns key-value pairs (field names and values); or <b>null</b> if no parsing should take place. Default is <b>null</b>.</value>
        /// <remarks><para>Parser method is required in cases when completion list is formatted so that it does not contain values from the data source or when
        /// the values of multiple fields are concatenated. Typically, this is accomplished by setting <see cref="ISearchEngine.PostProcess"/>
        /// method which modifies the and formats the values returned by <see cref="ISearchEngine"/> object.</para>
        /// <para>The method should return <b>null</b> or empty dictionary if exact parse of all fields cannot be performed.
        /// Otherwise, the behavior of <b>SearchWs</b> is undefined.</para></remarks>
        /// <exception cref="ArgumentException">Generated if the provided method is not static.</exception>
        public Delegates.Function<IDictionary<string, string>, string> SearchFieldParser
        {
            get { return this.searchFieldParser; }
            set { SetSearchFieldParser(value); }
        }

        private void SetSearchFieldParser(Delegates.Function<IDictionary<string, string>, string> value)
        {
            if (value == null)
            {
                this.searchFieldParser = null;
            }
            else if (!value.Method.IsStatic)
            {
                throw new ArgumentException("The provided SearchFieldParser method must be static.");
            }
            else
            {
                this.searchFieldParser = value;
            }
        }

        #endregion

        #region ReadFromDictionary.

        /// <summary>Reads values from a dictionary and stores them into <b>SearchWsConfiguration</b> properties.</summary>
        /// <param name="extendedProperties">A dictionary which contains all or some of the values required to build <see cref="SearchWsConfiguration"/> object.</param>
        public void ReadFromDictionary(IDictionary<string, object> extendedProperties)
        {
            string fieldList = Convert.ToString(TryGetExtendedProperty(extendedProperties, Keys.SearchFieldList));
            if (!string.IsNullOrEmpty(fieldList))
                this.SearchFields = fieldList.Split(';');

            var parserMethod = ReflectionUtil.TryCreateDelegate<Delegates.Function<IDictionary<string, string>, string>>(
                Convert.ToString(TryGetExtendedProperty(extendedProperties, Keys.SearchFieldParserAssembly)),
                Convert.ToString(TryGetExtendedProperty(extendedProperties, Keys.SearchFieldParserClass)),
                Convert.ToString(TryGetExtendedProperty(extendedProperties, Keys.SearchFieldParserMethod)));
            SetSearchFieldParser(parserMethod);
        }

        private static object TryGetExtendedProperty(IDictionary<string, object> extendedProperties, string key)
        {
            object value;
            extendedProperties.TryGetValue(key, out value);
            return value;
        }

        #endregion

        #region WriteToDictionary.

        /// <summary>Writes property values to a dictionary.</summary>
        /// <param name="extendedProperties">A dictionary which contains all or some of the values required to build <see cref="SearchWsConfiguration"/> object.</param>
        public void WriteToDictionary(IDictionary<string, object> extendedProperties)
        {
            if (!ArrayUtil.IsNullOrEmpty(this.SearchFields))
                extendedProperties[Keys.SearchFieldList] = ArrayUtil.Concat(this.SearchFields, ";");

            extendedProperties[Keys.SearchFieldParserAssembly] = (this.SearchFieldParser != null) ? this.SearchFieldParser.Method.DeclaringType.Assembly.FullName : null;
            extendedProperties[Keys.SearchFieldParserClass] = (this.SearchFieldParser != null) ? this.SearchFieldParser.Method.DeclaringType.FullName : null;
            extendedProperties[Keys.SearchFieldParserMethod] = (this.SearchFieldParser != null) ? this.SearchFieldParser.Method.Name : null;
        }

        #endregion
    }
}
